#!/usr/local/bin/python
# Rob Dennis - Lurchington Q. Delacroix
# Keasby - TV renamer
# Project Homepage: http://code.google.com/p/keasby/

import logging
import unittest
from string import Template
from keasby_toolbox import swap_keys
from base_classes import KeasbyTestCase

class TvEpisode():
    def __init__(self, input_dict={}, mapping_dict=None):
        """
        TvEpisode class represent a singular unit of television. Optional
        parameter mapping_dict is used to reshape the keys of the supplied
        input dictionary to conform to TvEpisode's desired format. All keys
        not in mapping_dict will be read in as necessary.
        """

        required_keys = ["showname","season_number","episode_number"]
        self.__dict__.fromkeys(required_keys)

        self.__dict__.update(swap_keys(input_dict, mapping_dict))

    def _remap(self, input_dict, mapping_dict):
        """
        Switching key names of an input dictionary for the tv_episode
        required names
        """

        mapped_dict = {}

        for tvep_key,input_key in mapping_dict.iteritems():
            mapped_dict[tvep_key] = input_dict[input_key]

        orig_pairs = [(key,value) for key,value in input_dict.iteritems()
                                    if key not in mapping_dict.values()]

        mapped_dict.update(dict(orig_pairs))

        self.__dict__.update(mapped_dict)

#---Modification operators

    def __add__(self, tvep):
        """
        Adds a new TvEpisode and returns the resulting TvEpisode instance
        """

        if not isinstance(tvep, TvEpisode):
            raise TvEpisodeError("Adding with %s not implemented"%type(tvep))

        return TvEpisode(self._add_dict(self.__dict__.copy(), tvep.__dict__))

    def __iadd__(self, tvep):
        """
        Adds a new TvEpisode instance in place
        """

        if not isinstance(tvep, TvEpisode):
            raise TvEpisodeError("Adding with %s not implemented"%type(tvep))

        self._add_dict(self.__dict__, tvep.__dict__)

        return self

    def merge(self, tvep):
        """
        Adds a new TvEpisode instance in place, suitable for use inside
        other other statements
        """

        self += tvep

        return self

    def expand(self, template, delimiter="-",
                season_padding = 0,
                episode_padding = 0):
        """
        Expands the given template using the information. Padding is the
        number of spaces you want the given number to be spaced to. The
        number can have more places then the padding, in which case, no
        modification will occur.
        Delimiter should be the character (or string) that seperates
        two grouped values. For example, for a delimiter of "-",
        no padding, and an episode_number of [1, 2], the display value
        of $episode_number is:
        1-2
        for [1, 2, 3]:
        1-2-3
        and for an episode_padding of 2, the result of [1, 2] is:
        01-02
        """

        try:
            modified_kwargs = self._shape_template(delimiter,
                                                    season_padding,
                                                    episode_padding)

            _template = Template(template).substitute(self.__dict__,
                                                        **modified_kwargs)
            return _template
        except KeyError, error_message:
            raise TvEpisodeError(error_message)

    def _shape_template(self, delimiter, season_padding, episode_padding):
        """
        Returns a dictionary with key:value pairs, suitable for outputting
        in a template for:
        - All keys that have a list as a value, eg. grouped. The grouped
            values will be seperated by delimiter.
        - episode number and season number, will be zero-padded by the
            values in episode_padding and season_padding, respectively
        """

        pad = dict(season_number = season_padding,
                        episode_number = episode_padding)

        expansion_gen = (self._expand_grouped(key, value, delimiter, pad.get(key))
                            for key, value in self.__dict__.iteritems())

        _tmp = {}

        for expanded_dict in expansion_gen:
            _tmp.update(expanded_dict)

        return _tmp

#--- Type-specific dictionary addition

    def _add_dict(self, basedict, newdict):
        """
        Updates basedict with newdict. For collided keys, create an array
        holding the base and new value. Or if the base value is a list,
        append the new value to that array. Returns the new basedict.
        """

        for key, value in newdict.iteritems():
            if key not in basedict:
                #there's a unique key, so add the key:value pair
                basedict[key] = value

            elif (isinstance(basedict[key],list) and
                    value not in basedict[key]):
                #there's a list already there, that doesn't hold that
                #value. So append the value
                basedict[key].append(value)

            elif basedict[key] != value:
                #there's a collision, where the values are different,
                #so create an array of the original and new value
                basedict[key] = [basedict[key], value]

            #if no statement is applicable, the original key is kept

        return basedict

#--- Utility operators

    def __str__(self):
        """
        Usable string implementation
        """

        return self._name()

    def __getitem__(self,key):
        """
        Accesses parts of the TvEpisode
        """

        return self.__dict__[key]

    def __cmp__(self, comparator):
        """
        Important for sorting TvEpisodes
        """

        if isinstance(comparator,TvEpisode):
            return cmp(int(self["episode_number"]),
                        int(comparator["episode_number"])
                        )
        else:
            return NotImplemented

    def __contains__(self, key):
        """
        Used to test for the existence of an attribute of a TvEpisode
        """

        return key in self.__dict__

#--- Utility

    def _name(self):
        """
        Used to make a default name for printing
        """

        _name_string = ["%(showname)s"%self.__dict__,
                        "S%(season_number)s"%self.__dict__,
                        "E%(episode_number)s"%self.__dict__,
                        "%(title)s"%self.__dict__]

        return " - ".join(_name_string)

#--- Exceptions

class TvEpisodeError(Exception):
    """
    Generic TvEpisode exception
    """
    pass

class TvEpisodeTestCase(KeasbyTestCase):
    """
    Tests the methods for the TvEpisode class
    """

    def setUp(self):
        """
        Sets up the test environment
        """

        _showname = "testshow"
        self.showname = _showname
        self.ep_base = [dict(showname=_showname,
                            season_number=1,
                            episode_number=ep,
                            title="%s#%s"%(_showname,ep))

                            for ep in xrange(1, 4)]

        self.tvep = [TvEpisode(ep_dict) for ep_dict in self.ep_base]
        self.ep_numbers = [ep_dict["episode_number"]
                                for ep_dict in self.ep_base]

        self.templates = ["${%s}"%key for key in self.ep_base[0].keys()]

    def test_add_basic(self):
        """
        Tests adding two different TvEpisode instances. Adding creates a new
        TvEpisode instance.
        """

        added_ep = self.tvep[0] + self.tvep[1]

        self.assertLog(added_ep, "the result for adding is invalid")
        self.assertLog(isinstance(added_ep, TvEpisode),
                        "the result is not a valid TvEpisode")

    def test_add_single(self):
        """
        Tests adding two different TvEpisode instances. Adding creates a new
        TvEpisode instance.
        """

        added_ep = self.tvep[0] + self.tvep[1]

        self.assertEquals(self.ep_numbers[:2], added_ep["episode_number"])
        self.assertEquals(self.showname, added_ep["showname"])

        for i in xrange(len(self.ep_base)):
            self.assertEquals(self.tvep[i],TvEpisode(self.ep_base[i]))

    def test_add_total(self):
        """
        Tests adding two different TvEpisode instances. Adding creates a new
        TvEpisode instance.
        """

        summed_ep = reduce(lambda x, y: x+y, self.tvep)
        self.assertEquals(summed_ep["episode_number"],self.ep_numbers)

        for i in xrange(len(self.ep_base)):
            self.assertEquals(self.tvep[i],TvEpisode(self.ep_base[i]))

    def test_iadd_basic(self):
        """
        Tests adding in place of two different TvEpisode instances.
        This modifies the base TvEpisode in the same manner as:
        base_ep = base_ep + new_ep
        """

        self.assertLog(self.tvep[0], "TvEpisode instance is originally invalid")
        self.assertEquals(self.tvep[0], TvEpisode(self.ep_base[0]))
        self.assertEquals(self.tvep[0]["episode_number"], self.ep_numbers[0])

        self.tvep[0] += self.tvep[1]

        self.assertLog(self.tvep, "TvEpisode list is invalid")
        self.assertLog(self.tvep[0], "TvEpisode instance is invalid")
        self.assertLog(self.tvep[0]["episode_number"],
                        "TvEpisode has invalid episode_number")

    def test_iadd_single(self):
        """
        Tests for add-in-place equality for a single operand
        """

        self.tvep[0] += self.tvep[1]
        self.assertEquals(self.tvep[0]["episode_number"], self.ep_numbers[:2])

    def test_iadd_total(self):
        """
        Tests adding-in-place equality for all TvEpisode instances.
        """

        for ep in self.tvep[1:]:
            self.tvep[0] += ep

        self.assertLog(isinstance(self.tvep[0],TvEpisode),
                        "TvEpisode instance is invalid")

        self.assertEquals(self.tvep[0]["episode_number"],self.ep_numbers)

    def test_merge_basic(self):
        """
        Should be indentical to += in all respects, but this allows the
        operator to be use as inside of other expressions. Repeats basic
        tests.
        """

        self.assertLog(self.tvep[0], "TvEpisode instance is originally invalid")
        self.assertEquals(self.tvep[0], TvEpisode(self.ep_base[0]))
        self.assertEquals(self.tvep[0]["episode_number"], self.ep_numbers[0])

        self.assertLog(self.tvep[0].merge(self.tvep[1]),
                        "TvEpisode instance is invalid")
        self.assertLog(self.tvep, "TvEpisode list is invalid")
        self.assertLog(self.tvep[0]["episode_number"],
                        "TvEpisode has invalid episode_number")

    def test_merge_single(self):
        """
        Tests for merge equality for a single operand
        """

        self.assertEquals(self.tvep[0].merge(self.tvep[1])["episode_number"],
                            self.ep_numbers[:2])

    def test_merge_total(self):
        """
        Tests merge equality for all TvEpisode instances.
        """

        for ep in self.tvep[1:]:
            self.tvep[0].merge(ep)

        self.assertLog(isinstance(self.tvep[0],TvEpisode),
                        "TvEpisode instance is invalid")

        self.assertEquals(self.tvep[0]["episode_number"],self.ep_numbers)

    def test_expand_single(self):
        """
        Tests for the correct expansion of a given string template
        """

        template = r"${showname}-S${episode_number}E${season_number}-${title}"

        expanded = [ep.expand(template) for ep in self.tvep]


        for result,base_info in zip(expanded, self.ep_base):
            self.assertLog(result, "invalid string", result)
            self.assertEquals(result, Template(template).substitute(base_info))

    def test_expand_merged(self):
        """
        Tests for the correct expansion of a given string template. The
        TvEpisode will be merged from 2-3 other TvEpisode instances
        """

        template = r"${showname}-S${season_number}E${episode_number}-${title}"
        summed_ep = reduce(lambda x, y: x+y, self.tvep)

        expanded = summed_ep.expand(template)
        self.assertLog(expanded, "invalid string", expanded)

        expanded2 = summed_ep.expand(template, delimiter="-")

    def test_expand_total(self):
        """
        Tests for the correct expansion of all(?) possible templates
        """

        specs = dict(delimiter = " + ",
                        season_padding = 2,
                        episode_padding = 2)

        for template in self.templates:
            for ep in self.tvep:
                self.assertLog(ep.expand(template, **specs))


class TvEpisodeInternalTestCase(KeasbyTestCase):
    """
    Tests the internal methods for the TvEpisode class
    """

    def setUp(self):
        """
        Instantiates a KeasbyOptions instance
        """

        _showname = "testshow"
        self.showname = _showname
        self.ep_base = [dict(showname=_showname,
                            season_number=1,
                            episode_number=ep,
                            title="%s#%s"%(_showname,ep))

                            for ep in xrange(1, 4)]

        self.tvep = [TvEpisode(ep_dict) for ep_dict in self.ep_base]
        self.merged = self.tvep[0] + self.tvep[1]

        self.ep_numbers = [ep_dict["episode_number"]
                                for ep_dict in self.ep_base]

        self.templates = ["${%s}"%key for key in self.ep_base[0].keys()]


    def test_shape_template(self):
        """
        Tests the result of shape_template
        """

        specs = dict(delimiter = " + ",
                        season_padding = 2,
                        episode_padding = 2)

        expected = {}

        expected.update(self._expand_grouped("episode_number",
                                                self.merged["episode_number"],
                                                specs["delimiter"],
                                                specs["episode_padding"]))

        expected.update(self._expand_grouped("season_number",
                                                self.merged["season_number"],
                                                specs["delimiter"],
                                                specs["season_padding"]))

        expected.update(self._expand_grouped("showname",
                                                    self.merged["showname"],
                                                    specs["delimiter"],
                                                    0))

        for key, value in expected.iteritems():
            self.assertLog(value, "value not valid for %s"%key)

            if not isinstance(self.merged[key], list):
                self.assertLog(specs["delimiter"] not in expected[key],
                                "%s improperly split"%expected[key],
                                "%s:%s"%(key,expected[key]))

            else:
                self.assertLog(specs["delimiter"] in expected[key],
                                "%s not properly split"%expected[key],
                                "%s:%s"%(key,expected[key]))

    @staticmethod
    def _expand_grouped(key_name, value, delimiter, padding=None):
        """
        Takes in a list of values and if it's an integer, pads
        each to length <padding>, then joining them with provided
        <delimiter>. Outputs a tuple of key_name, and the result of
        the expansion. If the result is invalid (IE, there were no
        changes needed to value list), return {}.
        """

        if padding is None:
            padding = 0

        if not isinstance(value, list):
            value_list = [value]
        else:
            value_list = value

        padded_list = [str(value).zfill(padding)
                        for value in value_list if str(value).isdigit()]

        if padded_list:
            return {key_name:str(delimiter).join(padded_list)}

        else:
            return {}

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG,
                        format="%(levelname)s: %(message)s")

    unittest_list = []
    unittest_list.append(TvEpisodeInternalTestCase)
    unittest_list.append(TvEpisodeTestCase)

    for unittest_class in unittest_list:
        suite = unittest.TestLoader().loadTestsFromTestCase(unittest_class)
        unittest.TextTestRunner(verbosity=2).run(suite)
